package org.currentcost;

import gnu.io.*;
import org.currentcost.messages.EnviMessage;
import org.currentcost.messages.EnviUnit;
import org.currentcost.messages.SensorData;
import org.xml.sax.*;
import org.xml.sax.helpers.DefaultHandler;
import org.xml.sax.helpers.XMLReaderFactory;

import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PushbackReader;
import java.text.ParseException;

public class EnviReader extends DefaultHandler implements EnviSchema
{
	public static final int PORT_TIMEOUT = 5000;
	public static final String CCSTREAM = "ccstream";
	private String _portName;
	private MessageConsumer _consumer;
	private static final int BAUD_RATE = 57600;
	private static final String PORT_OWNER = "EnviReader";
	private static final String COM = "COM";
	private EnviMessage _message;
	private StringBuilder _charBuf;
	private String _channelId;
	private PushbackReader _portReader;
	private boolean _stopRequested;
	private SerialPort _serialPort;

	public EnviReader(String portName, MessageConsumer consumer)
	{
		_portName = portName;
		_consumer = consumer;
	}

	public void start() throws SAXException, UnsupportedCommOperationException, NoSuchPortException,
	                           PortInUseException, IOException
	{
		_serialPort = openPort();
		_portReader = new PushbackReader(new InputStreamReader(_serialPort.getInputStream()), 32);
		_portReader.unread("<ccstream>".toCharArray());
		_stopRequested = false;
		_restartParsing();
		//		BufferedReader reader = new BufferedReader(new InputStreamReader(port.getInputStream()));
		//		String line;
		//		for (line = reader.readLine(); line != null; line = reader.readLine()) {
		//			System.out.println(line);
		//		}
	}

	public void stop()
	{
		_stopRequested = true;
	}

	private void _restartParsing() throws SAXException
	{
		/* prepare the XML parser */
		XMLReader parser = XMLReaderFactory.createXMLReader();
		parser.setContentHandler(this);
		try {
			parser.parse(new InputSource(_portReader));
		}
		catch (Exception e) {
			if (!_stopRequested) {
				e.printStackTrace();
				_restartParsing();
			}
		}
		finally {
			_serialPort.close();
		}
	}

	private SerialPort openPort() throws NoSuchPortException, UnsupportedCommOperationException, PortInUseException
	{
		String portName = (_portName.matches("[0-9]+")) ?
				COM + _portName : _portName;

		/* specify the parameters to connect to the CurrentCost meter */
		CommPortIdentifier portId = CommPortIdentifier.getPortIdentifier(portName);

		SerialPort port = (SerialPort) portId.open(PORT_OWNER, PORT_TIMEOUT);

		port.setSerialPortParams(BAUD_RATE,
		                         SerialPort.DATABITS_8,
		                         SerialPort.STOPBITS_1,
		                         SerialPort.PARITY_NONE);

		/* specify this to handle issues using USB-to-serial connector */
		port.notifyOnOutputEmpty(true);

		return port;
	}

	// XML Parsing

	@Override
	public void startElement(String uri, String localName, String qName, Attributes attributes) throws SAXException
	{
		if (_stopRequested) {
			throw new StopRequestedException();
		}
		if (CCSTREAM.equals(localName)) {
			// do nothing
		}
		else if (MSG.equals(localName)) {
			if (_message != null) {
				throw new SAXParseException("Received msg start element while parsing message", null);
			}
			_message = new EnviMessage();
			_message.setType(EnviMessage.Type.RealTime);
		}
		else if (VERSION.equals(localName) ||
				DAYS_SINCE_BIRTH.equals(localName) ||
				UID.equals(localName) ||
				TIME.equals(localName) ||
				TEMPERATURE_CELSIUS.equals(localName) ||
				TEMPERATURE_FARENHEIT.equals(localName) ||
				APPLIANCE_NUMBER.equals(localName) ||
				RADIO_ID.equals(localName) ||
				SENSOR_TYPE.equals(localName) ||
				DAYS_SINCE_WIPE.equals(localName) ||
				WATTS.equals(localName) ||
				UNITS.equals(localName) ||
				HOUR_RANGE_PATTERN.matcher(localName).matches() ||
				DAYS_AGO_PATTERN.matcher(localName).matches()) {
			_newCharBuffer();
		}
		else if (SENSOR_CHANNEL_PATTERN.matcher(localName).matches()) {
			_channelId = localName.substring(SENSOR_CHANNEL_PREFIX.length());
		}
		else if (HISTORY.equals(localName)) {
			// do nothing
		}
		else if (DATA.equals(localName)) {
			// do nothing
		}
		else {
			log("Unrecognized element: " + localName);
		}
	}

	private void _newCharBuffer() throws SAXException
	{
		if (_charBuf != null) {
			throw new SAXException("Discarded unmatched text data: " + new String(_charBuf));
		}
		_charBuf = new StringBuilder();
	}

	@Override
	public void endElement(String uri, String localName, String qName) throws SAXException
	{
		if (_stopRequested) {
			throw new StopRequestedException();
		}
		if (MSG.equals(localName)) {
			_consumer.messageReceived(_message);
			_message = null;
		}
		else if (VERSION.equals(localName)) {
			_message.setVersion(_charBuf.toString());
			_charBuf = null;
		}
		else if (DAYS_SINCE_BIRTH.equals(localName)) {
			_message.setDaysSinceBirth(_parseInt());
		}
		else if (UID.equals(localName)) {
			_message.setUid(_charBuf.toString());
		}
		else if (APPLIANCE_NUMBER.equals(localName)) {
			_message.setSensorId(_charBuf.toString());
		}
		else if (TIME.equals(localName)) {
			try {
				_message.setTimeString(_charBuf.toString());
			}
			catch (ParseException e) {
				throw new SAXException("Could not parse time: " + _charBuf.toString());
			}
		}
		else if (TEMPERATURE_CELSIUS.equals(localName)) {
			_message.setTemperature(_parseDouble(), EnviUnit.Celsius);
		}
		else if (TEMPERATURE_FARENHEIT.equals(localName)) {
			_message.setTemperature(_parseDouble(), EnviUnit.Farenheit);
		}
		else if (RADIO_ID.equals(localName)) {
			_message.setRadioId(_charBuf.toString());
		}
		else if (SENSOR_TYPE.equals(localName)) {
			switch (_parseInt()) {
				case 1:
					_message.setSensorType(SensorData.SensorType.ELECTRICITY);
					break;
				case 2:
					_message.setSensorType(SensorData.SensorType.IMPULSE_ELECTRIC);
					break;
				case 3:
					_message.setSensorType(SensorData.SensorType.IMPULSE_GAS);
					break;
				case 4:
					_message.setSensorType(SensorData.SensorType.IMPULSE_WATER);
					break;
				default:
					throw new SAXException("Unrecognized sensor type: " + _charBuf.toString());
			}
		}
		else if (SENSOR_CHANNEL_PATTERN.matcher(localName).matches()) {
			String channelId = localName.substring(SENSOR_CHANNEL_PREFIX.length());
			if (!channelId.equals(_channelId)) {
				throw new SAXException("End tag " + localName + " doesn't match current channel ID " +
						                       ((_channelId == null) ? "null" : _channelId));
			}
			_channelId = null;
		}
		else if (WATTS.equals(localName)) {
			_message.addData(_channelId, _parseDouble(), EnviUnit.W);
		}
		else if (IMPULSE.equals(localName)) {
			_message.addData(_channelId, _parseDouble(), EnviUnit.Impulses);
		}
		else if (IMPULSE_PER_UNIT.equals(localName)) {
			_message.setImpulsePerUnit(_parseDouble());
		}
		else if (DAYS_SINCE_WIPE.equals(localName)) {
			_message.setDaysSinceWipe(_parseInt());
		}
		else if (UNITS.equals(localName)) {
			String unit = _charBuf.toString();
			if (KWHR.equals(unit)) {
				_message.setUnit(EnviUnit.kWh);
			}
		}
		else if (DATA.equals(localName)) {
			_message.setSensorId(null);
		}
		else if (HOUR_RANGE_PATTERN.matcher(localName).matches()) {
			Integer hoursAgo = Integer.parseInt(localName.substring(HOUR_RANGE_PREFIX.length()));
			_message.setType(EnviMessage.Type.Hourly);
			_message.addHourData(_channelId, hoursAgo, _parseDouble());
		}
		else if (DAYS_AGO_PATTERN.matcher(localName).matches()) {
			Integer daysAgo = Integer.parseInt(localName.substring(DAYS_AGO_PREFIX.length()));
			_message.setType(EnviMessage.Type.Daily);
			_message.addDayData(_channelId, daysAgo, _parseDouble());
		}
		_charBuf = null;
	}

	private double _parseDouble()
	{
		return Double.parseDouble(_charBuf.toString());
	}

	private int _parseInt()
	{
		return Integer.parseInt(_charBuf.toString());
	}

	@Override
	public void characters(char[] ch, int start, int length) throws SAXException
	{
		if (_charBuf == null) {
			for (int i = start; i < start + length; i++) {
				char c = ch[i];
				if (!Character.isWhitespace(c) && c != '\n' && c != '\r') {
					System.err.println("Received unparsed chars: \"" +
							                   new String(ch, start, length) + "\"");
					return;
				}
			}
			return;
		}
		_charBuf.append(new String(ch, start, length));
	}

	private void log(String msg)
	{
		System.out.println(msg);
	}

	private static class StopRequestedException extends SAXException
	{
	}
}
